
// Copyright (C) 2011 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

using Derm.OpenGL;

namespace Derm.Render
{
	/// <summary>
	/// Class responsible of keep track of shared shader source strings.
	/// </summary>
	/// <remarks>
	/// 
	/// </remarks>
	static class ShaderIncludeTree
	{
		#region Shader Include Strings Management

		public static void CreateShaderInclude(RenderContext ctx, IShaderInclude sInclude)
		{
			if (sInclude == null)
				throw new ArgumentNullException("sInclude");
			if (sIncludeSourceTree.ContainsKey(sInclude.IncludePath) == true)
				throw new ArgumentException(String.Format("the path {0} is in conflict", sInclude.IncludePath), "sInclude");

			// Register shader include
			sIncludeSourceTree[sInclude.IncludePath] = sInclude;

			// Name the include source, if driver supports it
			if ((ctx != null) && (ctx.Caps.ShaderInclude.Supported == true)) {
				StringBuilder sb = new StringBuilder();

				// Build include source string
				foreach (string line in sInclude.IncludeSource)
					sb.AppendLine(line);

				// Create shader
				Gl.NamedStringARB(Gl.SHADER_INCLUDE_ARB, -1, sInclude.IncludePath, -1, sb.ToString());
				RenderException.CheckErrors();
			}
		}

		public static bool ExistsShaderInclude(RenderContext ctx, IShaderInclude sInclude)
		{
			if ((ctx != null) && (ctx.Caps.ShaderInclude.Supported == true)) {
				if (ctx.IsCurrent() == false)
					throw new ArgumentException("not current", "ctx");

				return (Gl.IsNamedStringARB(sInclude.IncludePath.Length, sInclude.IncludePath));
			} else
				return (sIncludeSourceTree.ContainsKey(sInclude.IncludePath));
		}

		/// <summary>
		/// Delete a specific shader include source.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/>
		/// </param>
		/// <param name="sInclude">
		/// A <see cref="IShaderInclude"/> which has to be deleted from the <see cref="ShaderIncludeTree"/>.
		/// </param>
		public static void DeleteShaderInclude(RenderContext ctx, IShaderInclude sInclude)
		{
			if (sInclude == null)
				throw new ArgumentNullException("sInclude");
			if (sIncludeSourceTree.ContainsKey(sInclude.IncludePath) == true)
				throw new ArgumentException(String.Format("the path {0} is in conflict", sInclude.IncludePath), "sInclude");

			// Register shader include
			sIncludeSourceTree.Remove(sInclude.IncludePath);

			// Delete named include source
			if ((ctx != null) && (ctx.Caps.ShaderInclude.Supported == true)) {
				// Delete shader object
				Gl.DeleteNamedStringARB(-1, sInclude.IncludePath);
				RenderException.CheckErrors();
			}
		}

		/// <summary>
		/// Delete all known shader include sources.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> which have associated shader includes.
		/// </param>
		public static void DeleteShaderIncludes(RenderContext ctx)
		{
			IEnumerable<string> incPaths = sIncludeSourceTree.Keys;

			foreach (string path in incPaths)
				DeleteShaderInclude(ctx, sIncludeSourceTree[path]);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public static IEnumerable<string> GetShaderIncludeTree()
		{
			List<string> treePaths = new List<string>(sIncludeSourceTree.Keys.Count);

			foreach (string key in sIncludeSourceTree.Keys)
				treePaths.Add(key);
			treePaths.Sort();

			return (treePaths);
		}

		/// <summary>
		/// Map between paths and include source strings.
		/// </summary>
		private static readonly Dictionary<string, IShaderInclude> sIncludeSourceTree = new Dictionary<string,IShaderInclude>();

		#endregion

		#region Shader Include Preprocessor

		/// <summary>
		/// 
		/// </summary>
		private class IncludeProcessorContext
		{
			/// <summary>
			/// Current preprocessor path.
			/// </summary>
			public string CurrentPath = String.Empty;
		}

		/// <summary>
		/// Process shader source lines to resolve #include directives.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> determining the shader compiler capabilities.
		/// </param>
		/// <param name="cctx">
		/// A <see cref="ShaderCompilerContext"/> that specify the compiler paramateres.
		/// </param>
		/// <param name="sSource">
		/// A <see cref="IEnumerable&lt;System.String&gt;"/> that specify the shader source lines. Null items in the enumeration
		/// will be ignored.
		/// </param>
		/// <returns>
		/// It returns the processed source lines <paramref name="sSource"/>, but without any #include directive. Each #include
		/// directive will be replaced by the corresponding text depending on <paramref name="cctx"/>.
		/// </returns>
		/// <remarks>
		/// <para>
		/// <para>
		/// In the case <paramref name="ctx"/> supports shader includes, the returned value equals to <paramref name="sSource"/> (there's
		/// no need to process shader source: it will be done by the shader compiler).
		/// </para>
		/// </para>
		/// </remarks>
		/// <exception cref="ArgumentNullException">
		/// Exception throw if <paramref name="ctx"/>, <paramref name="cctx"/> or <paramref name="sSource"/> is null.
		/// </exception>
		public static List<string> ProcessIncludeDirectives(RenderContext ctx, ShaderCompilerContext cctx, List<string> sSource)
		{
			if (ctx.Caps.ShaderInclude.Supported == false) {
				IncludeProcessorContext ictx = new IncludeProcessorContext();

				return (ProcessIncludeDirectives(ctx, cctx, ictx, sSource));
			} else
				return (sSource);
		}

		private static List<string> ProcessIncludeDirectives(RenderContext ctx, ShaderCompilerContext cctx, IncludeProcessorContext ictx, IEnumerable<string> sSource)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (cctx == null)
				throw new ArgumentNullException("cctx");
			if (sSource == null)
				throw new ArgumentNullException("sSource");

			List<string> processedSource = new List<string>();

			// Note: shader includes not supported. Process them manually before submitting shader source text lines.

			foreach (string line in sSource) {
				// Ignore null items
				if (line == null) continue;

				if ((sIncludeRegex.Match(line)).Success == true) {
					IShaderInclude shaderInclude = null;
					string includePath = ExtractIncludePath(line);
					string canonicalPath = String.Empty;

					if (includePath.StartsWith("/") == false) {

						// If <path> does not start with a forward slash, it is a path relative
						// to one of the ordered list of initial search points.

						if ((ictx.CurrentPath != String.Empty) && (sIncludeRegexAngular.Match(line).Success == false)) {

							// If it is quoted with double quotes in a previously included string, then the first
							// search point will be the tree location where the previously included
							// string had been found. If not found there, the search continues at
							// the beginning of the list of search points, as just described (see comment later).

							canonicalPath = NormalizeIncludePath(Path.Combine(ictx.CurrentPath, includePath));
							if (sIncludeSourceTree.ContainsKey(canonicalPath) == true)
								shaderInclude = sIncludeSourceTree[canonicalPath];
						}

						// If this path is quoted with angled brackets, the tree is searched relative to the
						// first search point in the ordered list, and then relative to each
						// subsequent search point, in order, until a matching path is found in
						// the tree. This is also the behavior if it is quoted with double
						// quotes in an initial (non-included) shader string.

						if (shaderInclude == null) {
							foreach (string includeSearchPath in cctx.Includes) {
								canonicalPath = NormalizeIncludePath(Path.Combine(includeSearchPath, includePath));
								if (sIncludeSourceTree.ContainsKey(canonicalPath) == true) {
									shaderInclude = sIncludeSourceTree[canonicalPath];
									break;
								}
							}
						}
					} else {

						// If <path> starts with a forward slash, whether it is quoted with
						// double quotes or with angled brackets, the list of search points is
						// ignored and <path> is looked up in the tree as described in Appendix
						// A.

						canonicalPath = includePath;
						if (sIncludeSourceTree.ContainsKey(includePath) == false)
							throw new InvalidOperationException(String.Format("absolute include path \"{0}\" not existing", includePath));
						shaderInclude = sIncludeSourceTree[includePath];
					}

					if (shaderInclude == null)
						throw new InvalidOperationException(String.Format("include path '{0}' not found", includePath));

					// Recurse on included source (it may contain other includes)
					IncludeProcessorContext ictxRecurse = new IncludeProcessorContext();

					System.Diagnostics.Debug.Assert(String.IsNullOrEmpty(canonicalPath) == false);
					ictxRecurse.CurrentPath = canonicalPath;

					processedSource.AddRange(ProcessIncludeDirectives(ctx, cctx, ictxRecurse, shaderInclude.IncludeSource));
				} else
					processedSource.Add(line);
			}

			return (processedSource);
		}

		private static string ExtractIncludePath(string directive)
		{
			if (directive == null)
				throw new ArgumentNullException("directive");

			// Parse included path
			string[] tokens = sIncludePathSplitRegex.Split(directive);

			// Remove empty lines
			tokens = Array.FindAll<string>(tokens, delegate (string s) { return (String.IsNullOrEmpty(s) == false); });
			if (tokens.Length != 2)
				throw new ArgumentException("include path contains double-quotes or angle-bracket character");

			// Process path
			return (tokens[1]);
		}

		private static string NormalizeIncludePath(string path)
		{
			if (path == null)
				throw new ArgumentNullException("path");
			if (path.StartsWith("/") == false)
				throw new ArgumentException("not rooted", "path");

			List<string> iPathStack = new List<string>();
			string[] tokens = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

			foreach (string token in tokens) {
				// '.' doesn't change path
				if (token == ".")
					continue;
				if (token == "..") {
					// root parent is root (specification misalignment?)
					if (iPathStack.Count == 0)
						continue;
					iPathStack.RemoveAt(iPathStack.Count - 1);
				} else {
					iPathStack.Add(token);
				}
			}

			// Concatenate filtered tokens
			StringBuilder sb = new StringBuilder();

			foreach (string token in iPathStack)
				sb.AppendFormat("/{0}", token);

			return (sb.ToString());
		}

		/// <summary>
		/// Regular expression used for recognizing #include preprocessor directives.
		/// </summary>
		private static readonly Regex sIncludeRegex = new Regex("#include *[<\"].*[>\"]", RegexOptions.Compiled);

		/// <summary>
		/// Regular expression used for recognizing #include preprocessor directives.
		/// </summary>
		private static readonly Regex sIncludeRegexAngular = new Regex("#include *[<\"].*[>\"]", RegexOptions.Compiled);

		/// <summary>
		/// Regular expression used for recognizing #include preprocessor directives.
		/// </summary>
		private static readonly Regex sIncludePathSplitRegex = new Regex("[<>\"\\n]", RegexOptions.Compiled);

		#endregion
	}
}
